<?php
/**
 * Simple PHP Contact Form
 *
 * This file is part of the Simple PHP Contact Form which is subject to the New
 * BSD License {@see LICENSE} which you would be advised to read before using,
 * modifying or redistributing this software.
 *
 * PHP version 5.2
 *
 * @category  Simple-PHP-Contact-Form
 * @package   Application_Library
 * @author    jah <jah@jahboite.co.uk>
 * @copyright 2010 jah <jah@jahboite.co.uk>
 * @license   New BSD License {@see LICENSE}
 * @version   SVN: $Id$
 * @link      http://code.google.com/p/simple-php-contact-form/
 */


/**
 * This Validator implements double submit cookie validation to help protect
 * against Cross Site Request Forgery.  It validates the value of a form field
 * (typically hidden, but certainly one that the end user in not invited to
 * modify) against the value of a cookie field which must match exactly as well
 * as conforming to length and character set constraints (e.g. exactly 40
 * alphanumeric characters).
 *
 * The inputs, from both POST and COOKIE arrays, are first canonicalised before
 * validation begins.
 *
 * @category  Simple-PHP-Contact-Form
 * @package   Application_Library
 * @author    jah <jah@jahboite.co.uk>
 * @copyright 2010 jah <jah@jahboite.co.uk>
 * @license   New BSD License {@see LICENSE}
 * @version   Release: @package_version@
 * @link      http://code.google.com/p/simple-php-contact-form/
 */
class Custom_Validate_CSRFToken extends Zend_Validate_Abstract
{
    const TOKENS_DIFFER  = 'TOKENS_DIFFER';
    const MISSING_COOKIE  = 'MISSING_COOKIE';
    const POST_BAD_LENGTH  = 'POST_BAD_LENGTH';
    const POST_BAD_CHARSET  = 'POST_BAD_CHARSET';
    const COOKIE_BAD_LENGTH  = 'POST_BAD_LENGTH';
    const COOKIE_BAD_CHARSET  = 'POST_BAD_CHARSET';

    protected $_messageTemplates = array(
        self::MISSING_COOKIE  => /* who's been at the cookie jar again? */
        'CSRF token was not received in the Cookie Header.',
        self::POST_BAD_LENGTH  =>
        'CSRF token received in POST Header was not of the expected length.',
        self::POST_BAD_CHARSET  =>
        'CSRF token received in POST Header contained invalid characters.',
        self::COOKIE_BAD_LENGTH  =>
        'CSRF token received in Cookie Header was not of the expected length.',
        self::COOKIE_BAD_CHARSET  =>
        'CSRF token received in Cookie Header contained invalid characters.',
        self::TOKENS_DIFFER  =>
        'CSRF token received in the Cookie Header does not match the one received in the POST Header.',
    );

    /**
     * The set of acceptable characters from which the input strings must be
     * comprised.
     * e.g. "0123456789abcdef"
     *
     * @var string
     */
    private $_expectedCharset = null;
    /**
     * The expected length of the input strings.
     *
     * @var integer
     */
    private $_expectedLen     = null;
    /**
     * The key of the $_COOKIE array value containing the COOKIE half of a
     * double submit cookie.
     *
     * @var unknown_type
     */
    private $_cookieName      = null;


    /**
     * Sets Validator options.
     *
     * @param string|array|Zend_Config $options Validator options.
     *
     * @return null
     */
    public function __construct($options = array())
    {
        if ($options instanceof Zend_Config) {

            $options = $options->toArray();

        } else if (!is_array($options)) {

            $options     = func_get_args();
            if (!empty($options)) {
                $temp['expectedLen'] = array_shift($options);
            }
            if (!empty($options)) {
                $temp['expectedCharset'] = array_shift($options);
            }
            if (!empty($options)) {
                $temp['cookieName'] = array_shift($options);
            }

            $options = $temp;
        }

        if (array_key_exists('expectedCharset', $options)) {
            $this->setExpectedCharset($options['expectedCharset']);
        }

        if (array_key_exists('expectedLen', $options)) {
            $this->setExpectedLen($options['expectedLen']);
        }

        if (array_key_exists('cookieName', $options)) {
            $this->setCookieName($options['cookieName']);
        }
    }


    /**
     * Set the string of acceptable characters from which the input strings
     * must be comprised.
     *
     * @param string $expectedCharset A whitelist of characters against which
     *                                to validate input.
     *
     * @return null
     */
    public function setExpectedCharset($expectedCharset)
    {
        $this->_expectedCharset = $expectedCharset;
        return;
    }


    /**
     * Set the expected length of the input strings.
     *
     * @param integer $expectedLen The expected length of valid input.
     *
     * @return null
     */
    public function setExpectedLen($expectedLen)
    {
        $this->_expectedLen = $expectedLen;
        return;
    }


    /**
     * Set name of the key of the $_COOKIE array value containing the COOKIE
     * half of the double submit cookie.
     *
     * @param string $cookieName The name of the cookie field containing the
     *                           COOKIE half of a double submit cookie.
     *
     * @return null
     */
    public function setCookieName($cookieName)
    {
        $this->_cookieName = $cookieName;
        return;
    }


    /**
     * Validates the POST half of a double submit cookie against the COOKIE half
     * and both against string length and character set constraints.
     *
     * @param string $value The POST half of a double submit cookie from, for
     *                      example a hidden HTML form field.
     *
     * @return null
     */
    public function isValid($value)
    {
        $auditor = ESAPI::getAuditor('App_Validate_Token');

        $canonicalPostToken = ESAPI::getEncoder()->canonicalize($value, false);

        $this->_setValue($canonicalPostToken);
        $isValid = false;

        $v_len = new Zend_Validate_StringLength(
            $this->_expectedLen, $this->_expectedLen
        );
        if ($v_len->isValid($canonicalPostToken) !== true) {
            $this->_error(self::POST_BAD_LENGTH);
            $auditor->warning(
                Auditor::SECURITY, false,
                $this->_messageTemplates[self::POST_BAD_LENGTH]
            );
            return false;
        }

        $v_regex = new Custom_Validate_Charset($this->_expectedCharset);
        if ($v_regex->isValid($canonicalPostToken) !== true) {
            $this->_error(self::POST_BAD_CHARSET);
            $auditor->warning(
                Auditor::SECURITY, false,
                $this->_messageTemplates[self::POST_BAD_CHARSET]
            );
            return false;
        }

        $controller = Zend_Controller_Front::getInstance();
        $req = $controller->getRequest();
        $cookieVal = $req->getCookie($this->_cookieName);

        $canonicalCookie = ESAPI::getEncoder()->canonicalize($cookieVal, false);

        if ($canonicalCookie === null) {
            $this->_error(self::MISSING_COOKIE);
            $auditor->warning(
                Auditor::SECURITY, false,
                $this->_messageTemplates[self::MISSING_COOKIE]
            );
            return false;
        }

        if ($v_len->isValid($canonicalCookie) !== true) {
            $this->_error(self::COOKIE_BAD_LENGTH);
            $auditor->warning(
                Auditor::SECURITY, false,
                $this->_messageTemplates[self::COOKIE_BAD_LENGTH]
            );
            return false;
        }

        if ($v_regex->isValid($canonicalCookie) !== true) {
            $this->_error(self::COOKIE_BAD_CHARSET);
            $auditor->warning(
                Auditor::SECURITY, false,
                $this->_messageTemplates[self::COOKIE_BAD_CHARSET]
            );
            return false;
        }

        $v_identical = new Zend_Validate_Identical($this->_value);

        if ($v_identical->isValid($canonicalCookie) !== true) {
            $this->_error(self::TOKENS_DIFFER);
            $auditor->warning(
                Auditor::SECURITY, false,
                $this->_messageTemplates[self::TOKENS_DIFFER]
            );
            return false;
        }

        return true;

    }
}
